package com.pandi.springbootinfluxdb.v02;

import org.influxdb.InfluxDBMapperException;
import org.influxdb.annotation.Column;
import org.influxdb.annotation.Measurement;
import org.influxdb.dto.QueryResult;

import java.lang.reflect.Field;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

public class InfluxDBResultMapper {
    private static final ConcurrentMap<String, ConcurrentMap<String, Field>> CLASS_FIELD_CACHE = new ConcurrentHashMap();
    private static final int FRACTION_MIN_WIDTH = 0;
    private static final int FRACTION_MAX_WIDTH = 9;
    private static final boolean ADD_DECIMAL_POINT = true;
    private static final DateTimeFormatter RFC3339_FORMATTER;

    public InfluxDBResultMapper() {
    }

    public <T> List<T> toPOJO(QueryResult queryResult, Class<T> clazz) throws InfluxDBMapperException {
        return this.toPOJO(queryResult, clazz, TimeUnit.MILLISECONDS);
    }

    public <T> List<T> toPOJO(QueryResult queryResult, Class<T> clazz, TimeUnit precision) throws InfluxDBMapperException {
        this.throwExceptionIfMissingAnnotation(clazz);
        String measurementName = this.getMeasurementName(clazz);
        return this.toPOJO(queryResult, clazz, measurementName, precision);
    }

    public <T> List<T> toPOJO(QueryResult queryResult, Class<T> clazz, String measurementName) throws InfluxDBMapperException {
        return this.toPOJO(queryResult, clazz, measurementName, TimeUnit.MILLISECONDS);
    }

    public <T> List<T> toPOJO(QueryResult queryResult, Class<T> clazz, String measurementName, TimeUnit precision) throws InfluxDBMapperException {
        Objects.requireNonNull(measurementName, "measurementName");
        Objects.requireNonNull(queryResult, "queryResult");
        Objects.requireNonNull(clazz, "clazz");
        this.throwExceptionIfResultWithError(queryResult);
        this.cacheMeasurementClass(clazz);
        List<T> result = new LinkedList();
        queryResult.getResults().stream().filter((internalResult) -> {
            return Objects.nonNull(internalResult) && Objects.nonNull(internalResult.getSeries());
        }).forEach((internalResult) -> {
            internalResult.getSeries().stream().filter((series) -> {
                return series.getName().equals(measurementName);
            }).forEachOrdered((series) -> {
                this.parseSeriesAs(series, clazz, result, precision);
            });
        });
        return result;
    }

    void throwExceptionIfMissingAnnotation(Class<?> clazz) {
        if (!clazz.isAnnotationPresent(Measurement.class)) {
            throw new IllegalArgumentException("Class " + clazz.getName() + " is not annotated with @" + Measurement.class.getSimpleName());
        }
    }

    void throwExceptionIfResultWithError(QueryResult queryResult) {
        if (queryResult.getError() != null) {
            throw new InfluxDBMapperException("InfluxDB returned an error: " + queryResult.getError());
        } else {
            queryResult.getResults().forEach((seriesResult) -> {
                if (seriesResult.getError() != null) {
                    throw new InfluxDBMapperException("InfluxDB returned an error with Series: " + seriesResult.getError());
                }
            });
        }
    }

    ConcurrentMap<String, Field> getColNameAndFieldMap(Class<?> clazz) {
        return (ConcurrentMap)CLASS_FIELD_CACHE.get(clazz.getName());
    }

    void cacheMeasurementClass(Class<?>... classVarAgrs) {
        Class[] var2 = classVarAgrs;
        int var3 = classVarAgrs.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            Class<?> clazz = var2[var4];
            if (!CLASS_FIELD_CACHE.containsKey(clazz.getName())) {
                ConcurrentMap<String, Field> initialMap = new ConcurrentHashMap();
                ConcurrentMap<String, Field> influxColumnAndFieldMap = (ConcurrentMap)CLASS_FIELD_CACHE.putIfAbsent(clazz.getName(), initialMap);
                if (influxColumnAndFieldMap == null) {
                    influxColumnAndFieldMap = initialMap;
                }

                for(Class c = clazz; c != null; c = c.getSuperclass()) {
                    Field[] var9 = c.getDeclaredFields();
                    int var10 = var9.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        Field field = var9[var11];
                        Column colAnnotation = (Column)field.getAnnotation(Column.class);
                        if (colAnnotation != null) {
                            ((ConcurrentMap)influxColumnAndFieldMap).put(colAnnotation.name(), field);
                        }
                    }
                }
            }
        }

    }

    String getMeasurementName(Class<?> clazz) {
        return ((Measurement)clazz.getAnnotation(Measurement.class)).name();
    }

    String getDatabaseName(Class<?> clazz) {
        return ((Measurement)clazz.getAnnotation(Measurement.class)).database();
    }

    String getRetentionPolicy(Class<?> clazz) {
        return ((Measurement)clazz.getAnnotation(Measurement.class)).retentionPolicy();
    }

    TimeUnit getTimeUnit(Class<?> clazz) {
        return ((Measurement)clazz.getAnnotation(Measurement.class)).timeUnit();
    }

    <T> List<T> parseSeriesAs(QueryResult.Series series, Class<T> clazz, List<T> result) {
        return this.parseSeriesAs(series, clazz, result, TimeUnit.MILLISECONDS);
    }

    <T> List<T> parseSeriesAs(QueryResult.Series series, Class<T> clazz, List<T> result, TimeUnit precision) {
        int columnSize = series.getColumns().size();
        ConcurrentMap colNameAndFieldMap = (ConcurrentMap)CLASS_FIELD_CACHE.get(clazz.getName());

        try {
            T object = null;
            Iterator var8 = series.getValues().iterator();

            while(var8.hasNext()) {
                List<Object> row = (List)var8.next();

                for(int i = 0; i < columnSize; ++i) {
                    Field correspondingField = (Field)colNameAndFieldMap.get(series.getColumns().get(i));
                    if (correspondingField != null) {
                        if (object == null) {
                            object = clazz.newInstance();
                        }

                        this.setFieldValue(object, correspondingField, row.get(i), precision);
                    }
                }

                if (series.getTags() != null && !series.getTags().isEmpty()) {
                    Iterator var14 = series.getTags().entrySet().iterator();

                    while(var14.hasNext()) {
                        Map.Entry<String, String> entry = (Map.Entry)var14.next();
                        Field correspondingField = (Field)colNameAndFieldMap.get(entry.getKey());
                        if (correspondingField != null) {
                            this.setFieldValue(object, correspondingField, entry.getValue(), precision);
                        }
                    }
                }

                if (object != null) {
                    result.add(object);
                    object = null;
                }
            }

            return result;
        } catch (IllegalAccessException | InstantiationException var13) {
            throw new InfluxDBMapperException(var13);
        }
    }

    <T> void setFieldValue(T object, Field field, Object value, TimeUnit precision) throws IllegalArgumentException, IllegalAccessException {
        if (value != null) {
            Class fieldType = field.getType();

            try {
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }

                if (!this.fieldValueModified(fieldType, field, object, value, precision) && !this.fieldValueForPrimitivesModified(fieldType, field, object, value) && !this.fieldValueForPrimitiveWrappersModified(fieldType, field, object, value)) {
                    String msg = "Class '%s' field '%s' is from an unsupported type '%s'.";
                    throw new InfluxDBMapperException(String.format(msg, object.getClass().getName(), field.getName(), field.getType()));
                }
            } catch (ClassCastException var8) {
                String msg = "Class '%s' field '%s' was defined with a different field type and caused a ClassCastException. The correct type is '%s' (current field value: '%s').";
                throw new InfluxDBMapperException(String.format(msg, object.getClass().getName(), field.getName(), value.getClass().getName(), value));
            }
        }
    }

    <T> boolean fieldValueModified(Class<?> fieldType, Field field, T object, Object value, TimeUnit precision) throws IllegalArgumentException, IllegalAccessException {
        if (String.class.isAssignableFrom(fieldType)) {
            field.set(object, String.valueOf(value));
            return true;
        } else if (Instant.class.isAssignableFrom(fieldType)) {
            Instant instant;
            if (value instanceof String) {
                instant = Instant.from(RFC3339_FORMATTER.parse(String.valueOf(value)));
            } else if (value instanceof Long) {
                instant = Instant.ofEpochMilli(this.toMillis((Long)value, precision));
            } else if (value instanceof Double) {
                instant = Instant.ofEpochMilli(this.toMillis(((Double)value).longValue(), precision));
            } else {
                if (!(value instanceof Integer)) {
                    throw new InfluxDBMapperException("Unsupported type " + field.getClass() + " for field " + field.getName());
                }

                instant = Instant.ofEpochMilli(this.toMillis(((Integer)value).longValue(), precision));
            }

            field.set(object, instant);
            return true;
        } else {
            return false;
        }
    }

    <T> boolean fieldValueForPrimitivesModified(Class<?> fieldType, Field field, T object, Object value) throws IllegalArgumentException, IllegalAccessException {
        if (Double.TYPE.isAssignableFrom(fieldType)) {
            field.setDouble(object, (Double)value);
            return true;
        } else if (Long.TYPE.isAssignableFrom(fieldType)) {
            field.setLong(object, ((Double)value).longValue());
            return true;
        } else if (Integer.TYPE.isAssignableFrom(fieldType)) {
            field.setInt(object, ((Double)value).intValue());
            return true;
        } else if (Boolean.TYPE.isAssignableFrom(fieldType)) {
            field.setBoolean(object, Boolean.valueOf(String.valueOf(value)));
            return true;
        } else {
            return false;
        }
    }

    <T> boolean fieldValueForPrimitiveWrappersModified(Class<?> fieldType, Field field, T object, Object value) throws IllegalArgumentException, IllegalAccessException {
        if (Double.class.isAssignableFrom(fieldType)) {
            field.set(object, value);
            return true;
        } else if (Long.class.isAssignableFrom(fieldType)) {
            field.set(object, ((Double)value).longValue());
            return true;
        } else if (Integer.class.isAssignableFrom(fieldType)) {
            field.set(object, ((Double)value).intValue());
            return true;
        } else if (Boolean.class.isAssignableFrom(fieldType)) {
            field.set(object, Boolean.valueOf(String.valueOf(value)));
            return true;
        } else {
            return false;
        }
    }

    private Long toMillis(long value, TimeUnit precision) {
        return TimeUnit.MILLISECONDS.convert(value, precision);
    }

    static {
        RFC3339_FORMATTER = (new DateTimeFormatterBuilder()).appendPattern("yyyy-MM-dd'T'HH:mm:ss").appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).appendZoneOrOffsetId().toFormatter();
    }
}
